/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.web.gwt.basics.controller;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

import net.sf.cglib.proxy.Factory;

import com.genia.toolbox.basics.bean.EnumerationIterator;
import com.genia.toolbox.basics.bean.Transformer;
import com.genia.toolbox.basics.process.ProxyHelper;
import com.genia.toolbox.basics.proxy.methods.interceptors.ExceptionLoggerMethodIntercetor;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * Spring controller that dispatches the GWT servicesByName request to the
 * correct implementation.
 */
public class GwtServiceController
    extends AbstractController
{

  /**
   * fake {@link ServletConfig} that allows gwt servlet to access their
   * {@link ServletContext}.
   */
  private class FakeServletConfig
      implements ServletConfig
  {

    /**
     * the {@link ServletContext} to pass back to the gwt servlets.
     */
    private transient final ServletContext servletContext;



    /**
     * constructor.
     * 
     * @param servletContext
     *          the {@link ServletContext} to pass back to the gwt servlets
     */
    private FakeServletConfig(ServletContext servletContext)
    {
      this.servletContext = servletContext;
    }



    /**
     * Returns a <code>String</code> containing the value of the named
     * initialization parameter, or <code>null</code> if the parameter does
     * not exist.
     * 
     * @param name
     *          a <code>String</code> specifying the name of the
     *          initialization parameter
     * @return a <code>String</code> containing the value of the
     *         initialization parameter
     * @see javax.servlet.ServletConfig#getInitParameter(java.lang.String)
     */
    public String getInitParameter(String name)
    {
      return null;
    }



    /**
     * Returns the names of the servlet's initialization parameters as an
     * <code>Enumeration</code> of <code>String</code> objects, or an empty
     * <code>Enumeration</code> if the servlet has no initialization
     * parameters.
     * 
     * @return an <code>Enumeration</code> of <code>String</code> objects
     *         containing the names of the servlet's initialization parameters
     * @see javax.servlet.ServletConfig#getInitParameterNames()
     */
    public Enumeration<String> getInitParameterNames()
    {
      return new EnumerationIterator<String>(new ArrayList<String>());
    }



    /**
     * Returns a reference to the {@link ServletContext} in which the caller is
     * executing.
     * 
     * @return a {@link ServletContext} object, used by the caller to interact
     *         with its servlet container
     * @see ServletContext
     * @see javax.servlet.ServletConfig#getServletContext()
     */
    public ServletContext getServletContext()
    {
      return servletContext;
    }



    /**
     * Returns the name of this servlet instance. The name may be provided via
     * server administration, assigned in the web application deployment
     * descriptor, or for an unregistered (and thus unnamed) servlet instance it
     * will be the servlet's class name.
     * 
     * @return the name of the servlet instance
     * @see javax.servlet.ServletConfig#getServletName()
     */
    public String getServletName()
    {
      return GwtServiceController.class.getName();
    }
  }

  /**
   * Gwt servlet that delegate to the right bean.
   */
  @SuppressWarnings("serial")
  private class GwtServlet
      extends RemoteServiceServlet
  {

    /**
     * Process a call originating from the given request. Uses the
     * {@link RPC#invokeAndEncodeResponse(Object, java.lang.reflect.Method, Object[])}
     * method to do the actual work.
     * <p>
     * Subclasses may optionally override this method to handle the payload in
     * any way they desire (by routing the request to a framework component, for
     * instance). The {@link HttpServletRequest} and {@link HttpServletResponse}
     * can be accessed via the {@link #getThreadLocalRequest()} and
     * {@link #getThreadLocalResponse()} methods.
     * </p>
     * This is public so that it can be unit tested easily without HTTP.
     * 
     * @param payload
     *          the UTF-8 request payload
     * @return a string which encodes either the method's return, a checked
     *         exception thrown by the method, or an
     *         {@link IncompatibleRemoteServiceException}
     * @throws SerializationException
     *           if we cannot serialize the response
     */
    @Override
    public String processCall(String payload)
        throws SerializationException
    {
      try {
        RPCRequest rpcRequest = RPC.decodeRequest(payload);
        RemoteService service = servicesByClass.get(rpcRequest.getMethod().getDeclaringClass());
        return RPC.invokeAndEncodeResponse(service, rpcRequest.getMethod(), rpcRequest.getParameters(), rpcRequest.getSerializationPolicy());
      }
      catch (IncompatibleRemoteServiceException ex) {
        getServletContext().log("An IncompatibleRemoteServiceException was thrown while processing this call.", ex);
        return RPC.encodeResponseForFailure(null, ex);
      }
    }

  }

  /**
   * a {@link Transformer} that allows to retrieve the {@link RemoteService}
   * associated to an implementation class.
   */
  private final class RemoteServiceByClassTransformer
      implements Transformer<Class<?>, RemoteService>
  {
    /**
     * the transformation method.
     * 
     * @param baseClass
     *          the object to convert
     * @return the converted object
     */
    public RemoteService transform(Class<?> baseClass)
    {
      for (RemoteService remoteService : services) {
        if (baseClass.isAssignableFrom(remoteService.getClass())) {
          return remoteService;
        }
      }
      return null;
    }
  }

  /**
   * the {@link GwtServlet} that does the work.
   */
  private transient volatile GwtServlet gwtServlet;

  /**
   * the services being handled.
   */
  private List<RemoteService> services = new ArrayList<RemoteService>();

  /**
   * the services indexed by their class.
   */
  private transient final Map<Class<?>, RemoteService> servicesByClass = ProxyHelper.getSafeMap(new ConcurrentHashMap<Class<?>, RemoteService>(), new RemoteServiceByClassTransformer());



  /**
   * returns the {@link GwtServlet} to dispatch to.
   * 
   * @param servletContext
   *          the actual {@link ServletContext}
   * @return the {@link GwtServlet} to dispatch to
   * @throws ServletException
   *           if an initialization error occured
   */
  private GwtServlet getGwtServlet(ServletContext servletContext)
      throws ServletException
  {
    if (gwtServlet == null) {
      synchronized (this) {
        // We have to test again because we did not have the lock before.
        if (gwtServlet == null) {
          GwtServlet gwtServlet = new GwtServlet();
          gwtServlet.init(new FakeServletConfig(servletContext));
          this.gwtServlet = gwtServlet;
        }
      }
    }
    return gwtServlet;
  }



  /**
   * the actual dispatcher.
   * 
   * @param request
   *          the HTTP request
   * @param response
   *          the HTTP response
   * @return <code>null</code>
   * @throws ServletException
   * @exception IOException
   *              If an input or output exception occurs
   * @exception ServletException
   *              if the HTTP request cannot be handled
   */
  @Override
  protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
      throws ServletException, IOException
  {
    getGwtServlet(request.getSession().getServletContext()).service(request, response);
    return null;
  }



  /**
   * setter for the services property.
   * 
   * @param services
   *          the services to set
   */
  public void setServices(List<RemoteService> services)
  {
    this.services = new ArrayList<RemoteService>();
    for (RemoteService service : services) {
      Factory factory = ProxyHelper.getProxyFactory(service.getClass());
      this.services.add((RemoteService) factory.newInstance(new ExceptionLoggerMethodIntercetor(service)));
    }
  }

}
